/*
 * pcicfg - PCI drivers & tools for accessing memory location, I/O ports and configuration space.
 * Copyright (C) 2013  Andrew Karasyov
 *
 * This library is free software: you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation, either
 * version 3 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

#include <linux/module.h>
#include <linux/pci.h>
#include <linux/ioctl.h>
#include <linux/fs.h>
#include <asm/uaccess.h>
#include <linux/slab.h>
#include <linux/mutex.h>
#include "pcicfg.h"

MODULE_LICENSE( "GPL" );
MODULE_AUTHOR( "Andrew Karasyov <andrew.karasyov@gmail.com>" );

/*******************************************************************************
**** IOCTL implementation
*******************************************************************************/

/** Local mutex to lock execution path for kernels without BKL */
struct mutex lock_mutex;


int PciCfgOpen( struct inode *n, struct file *f )
{
   return 0;
}


int PciCfgRelease( struct inode *n, struct file *f )
{
   return 0;
}


ssize_t PciCfgRead( struct file * file, char * buf, size_t count, loff_t *ppos )
{
	return 0;
}


/* Operation prototypes */
unsigned int GetPciDeviceCount( void );
int GetPciDeviceList( unsigned long user_arg );
void GetPciDeviceArray( struct PCIDeviceDescription* device_array, int array_length );
int ReadCfgRegister( unsigned int cmd, unsigned long user_arg);
int ReadCfgSpace( unsigned long user_arg );
int WriteCfgRegister( unsigned int cmd, unsigned long user_arg);
int WriteCfgSpace( unsigned long user_arg );
int GetCfgSpace( unsigned short bdf, unsigned int offset_bytes, unsigned int size_bytes, unsigned char* out_buffer );


long PciCfgIoctl( struct file *f, unsigned int cmd, unsigned long arg )
{
	int err = 0;
	int retval = 0;
	
	/*
	 * Extract the type and number bitfields, and don't decode
	 * wrong cmds: return ENOTTY (inappropriate ioctl) before access_ok( )
	 */
	 if (_IOC_TYPE(cmd) != PCICFG_IOC_MAGIC ) 
		 return -ENOTTY;
	 
	 if (_IOC_NR(cmd) > PCICFG_IOC_MAXNR ) 
		 return -ENOTTY;
	
	/*
	 * The direction is a bitmask, and VERIFY_WRITE catches R/W
	 * transfers. `Type' is user-oriented, while
	 * access_ok is kernel-oriented, so the concept of "read" and
	 * "write" is reversed
	 */
	if ( _IOC_DIR(cmd) & _IOC_READ )
		err = !access_ok( VERIFY_WRITE, (void __user *)arg, _IOC_SIZE(cmd) );
	else if ( _IOC_DIR(cmd) & _IOC_WRITE )
		err =  !access_ok( VERIFY_READ, (void __user *)arg, _IOC_SIZE(cmd) );
	
	if ( err ) 
		return -EFAULT;

	/** For kernels without BKL do local lock */
	mutex_lock(&lock_mutex);

	switch ( cmd )
	{
		case PCICFG_IOCQDEVICECOUNT: /* Query: return it (it's positive) */
			retval = GetPciDeviceCount();
			break;
	    
		case PCICFG_IOCGDEVICELIST: /* Get: arg is pointer to result */
			retval = GetPciDeviceList( arg );
			break;
		
		case PCICFG_IOCXCFGBYTE:
		case PCICFG_IOCXCFGWORD:
		case PCICFG_IOCXCFGDWORD:
			retval = ReadCfgRegister( cmd, arg );
			break;

		case PCICFG_IOCXCFG4K:
			retval = ReadCfgSpace( arg );
			break;

		case PCICFG_IOCSCFGBYTE:
		case PCICFG_IOCSCFGWORD:
		case PCICFG_IOCSCFGDWORD:
			retval = WriteCfgRegister( cmd, arg );
			break;

		case PCICFG_IOCSCFG4K:
			retval = WriteCfgSpace( arg );
			break;

		default:  /* redundant, as cmd was checked against MAXNR */
			retval = -ENOTTY;
	}
	
	mutex_unlock(&lock_mutex);
	return retval;	    	

}

unsigned int GetPciDeviceCount( void )
{
	unsigned int device_count = 0;
	struct pci_dev *pdev = NULL;
	
	/* Either of the following looping constructs will work */
	for_each_pci_dev( pdev )
	/* while ((pdev = pci_get_device( PCI_ANY_ID, PCI_ANY_ID, pdev)) ) */
		++device_count;
	
	return device_count;
}


int GetPciDeviceList( unsigned long user_arg )
{
	/* User should pass on array of PCIDeviceDescription as a parameter.
	 * Array should be big enough to contain descriptions of all pci devices
	 */

	/* Check if any pci device */
	unsigned int pci_dev_count = GetPciDeviceCount();

	int retval = -EFAULT;
	if ( pci_dev_count )
	{
		/* Allocates memory for device list and get pci device descriptions. */
		int pci_device_array_size = pci_dev_count * sizeof (struct PCIDeviceDescription);
		struct PCIDeviceDescription* deviceArray = (struct PCIDeviceDescription*)kmalloc( pci_device_array_size, GFP_KERNEL );
		if ( deviceArray )
		{
			memset( deviceArray, 0, pci_device_array_size);
			GetPciDeviceArray( deviceArray, pci_dev_count );

			/* Copy device array into user space */
			if ( copy_to_user((void __user *)user_arg, (void*)deviceArray, pci_device_array_size) )
				retval = -EINVAL;
			else
				retval = pci_dev_count;

			/* Free allocated memory */
			kfree( deviceArray );
		}
	}
	else
	{
		retval = 0;
		printk( KERN_INFO "%s: there is no pci devices to return.\n", PCICFG_MODNAME);
	}

	return retval;
}


void GetPciDeviceArray( struct PCIDeviceDescription* device_array, int array_length )
{
	int idx = 0;
	struct pci_dev *pdev = NULL;
	u8 bus_number = 0;
	u8 dev_number = 0;
	u8 fn_number  = 0;
	
	/* Either of the following looping constructs will work */
	for_each_pci_dev( pdev )
	/* while ((pdev = pci_get_device( PCI_ANY_ID, PCI_ANY_ID, pdev)) ) */
	{
		if ( idx >= array_length )
			return;

		bus_number = (( pdev->bus != NULL ) ? pdev->bus->number : 0);
		dev_number = PCICFG_GET_DEV( pdev->devfn );
		fn_number  = PCICFG_GET_FUNC( pdev->devfn );
		
		device_array[idx].BDF = PCICFG_MAKE_BUSDEVFN( bus_number, dev_number, fn_number );
		device_array[idx].VendorID = pdev->vendor;
		device_array[idx].DeviceID = pdev->device;
		device_array[idx].SubsystemVendorID = pdev->subsystem_vendor;
		device_array[idx].SubsystemDeviceID = pdev->subsystem_device;
		device_array[idx].Class = pdev->class;
		device_array[idx].Revision = pdev->revision;
		device_array[idx].HeaderType = pdev->hdr_type;
		device_array[idx].PcieCapabilityOffset = pdev->pcie_cap;
		device_array[idx].PMCapabilityOffset = pdev->pm_cap;
		device_array[idx].CfgSpaceSize = pdev->cfg_size;
		device_array[idx].IsCfgAccessBlocked = pdev->block_cfg_access;
		device_array[idx].IsBridge = (pdev->subordinate ? 1 : 0);
		device_array[idx].IsTransparentBridge = pdev->transparent;
		device_array[idx].IsMultifunction = pdev->multifunction;
		device_array[idx].IsPcie = pci_is_pcie( pdev );
		device_array[idx].SubordinateBus = (pdev->subordinate ? pdev->subordinate->number : 0);

		++idx;
	}
}


int ReadCfgRegister( unsigned int cmd, unsigned long user_arg)
{
	int retval = -EINVAL;

	/* User should pass on this structure as a parameter */
	struct PCIDeviceValue pci_dev_val;
	size_t pci_device_value_size = sizeof(struct PCIDeviceValue);
	memset( &pci_dev_val, 0, pci_device_value_size);

	if ( !copy_from_user( &pci_dev_val, (void __user *)user_arg, pci_device_value_size ) )
	{
		struct pci_dev* pdev = pci_get_domain_bus_and_slot( 0, PCICFG_GET_BUS(pci_dev_val.BDF), PCICFG_GET_DEVFN(pci_dev_val.BDF));
		if ( pdev )
		{
            int offset = (int)(pci_dev_val.Offset);
			switch( cmd )
			{
				case PCICFG_IOCXCFGBYTE:
					retval = pci_read_config_byte( pdev, offset, &pci_dev_val.Byte0 );
					break;

				case PCICFG_IOCXCFGWORD:
					retval = pci_read_config_word( pdev, offset * 2, &pci_dev_val.Word0 );
					break;

				case PCICFG_IOCXCFGDWORD:
					retval = pci_read_config_dword( pdev, offset * 4, &pci_dev_val.Dword );
					break;

				default:
					printk( KERN_INFO "%s: Reading configuration space register failed. Wrong command.\n", PCICFG_MODNAME);
			}
		}
	}

	// Copy result to the user.
	if (retval >= 0)
		if ( copy_to_user( (void __user *)user_arg, (void*)&pci_dev_val, pci_device_value_size ) )
			retval = -EINVAL;

	return retval;
}


int ReadCfgSpace( unsigned long user_arg )
{
	/* User should pass on PCIDeviceValue with buffer allocated for max cfg space */

	int retval = -EINVAL;

	struct PCIDeviceValue pci_dev_val;
	size_t pci_device_value_size = sizeof(struct PCIDeviceValue);
	unsigned char* buffer4k = (unsigned char*)kmalloc( PCICFG_CFGSPACE_LENGTH_MAX_BYTES, GFP_KERNEL );
	memset( &pci_dev_val, 0, pci_device_value_size);
	memset( buffer4k, 0, PCICFG_CFGSPACE_LENGTH_MAX_BYTES );

	/* Gets user structure */
	if ( !copy_from_user( &pci_dev_val, (void __user *)user_arg, pci_device_value_size ) )
		retval = GetCfgSpace(pci_dev_val.BDF, 0, PCICFG_CFGSPACE_LENGTH_MAX_BYTES, buffer4k);
	else
		printk( KERN_INFO "%s: reading cfg space operation: copy_from_user failed\n", PCICFG_MODNAME);

	// Copy result to the user.
	if (retval >= 0)
	{
		if ( copy_to_user( (void __user *)(pci_dev_val.Buffer), (void*)buffer4k, PCICFG_CFGSPACE_LENGTH_MAX_BYTES ) )
		{
			retval = -EINVAL;
			printk( KERN_INFO "%s: reading cfg space operation: copy_to_user failed\n", PCICFG_MODNAME);
		}
	}
	else
		printk( KERN_INFO "%s: reading cfg space failed\n", PCICFG_MODNAME);

	kfree( buffer4k );

	return retval;
}


int WriteCfgRegister( unsigned int cmd, unsigned long user_arg)
{
	int retval = -EINVAL;

	/* User should pass on this structure as a parameter */
	struct PCIDeviceValue pci_dev_val;
	size_t pci_device_value_size = sizeof(struct PCIDeviceValue);
	memset( &pci_dev_val, 0, pci_device_value_size);

	if ( !copy_from_user( &pci_dev_val, (void __user *)user_arg, pci_device_value_size ) )
	{
		struct pci_dev* pdev = pci_get_domain_bus_and_slot( 0, PCICFG_GET_BUS(pci_dev_val.BDF), PCICFG_GET_DEVFN(pci_dev_val.BDF));
		if ( pdev )
		{
            int offset = (int)(pci_dev_val.Offset);
			switch( cmd )
			{
				case PCICFG_IOCSCFGBYTE:
					retval = pci_write_config_byte( pdev, offset, pci_dev_val.Byte0 );
					break;

				case PCICFG_IOCSCFGWORD:
					retval = pci_write_config_word( pdev, offset * 2, pci_dev_val.Word0 );
					break;

				case PCICFG_IOCSCFGDWORD:
					retval = pci_write_config_dword( pdev, offset * 4, pci_dev_val.Dword );
					break;

				default:
					printk( KERN_INFO "%s: Writing configuration space register failed. Wrong command.\n", PCICFG_MODNAME);
			}

		}
	}

	return retval;
}


int WriteCfgSpace( unsigned long user_arg )
{
	/* User should pass on PCIDeviceValue with buffer allocated for max cfg space */

	int retval = -EINVAL;

	struct PCIDeviceValue pci_dev_val;
	size_t pci_device_value_size = sizeof(struct PCIDeviceValue);
	unsigned char* buffer4k = (unsigned char*)kmalloc( PCICFG_CFGSPACE_LENGTH_MAX_BYTES, GFP_KERNEL );
	memset( &pci_dev_val, 0, pci_device_value_size);
	memset( buffer4k, 0, PCICFG_CFGSPACE_LENGTH_MAX_BYTES );

	/* Gets user structure */
	if ( !copy_from_user( &pci_dev_val, (void __user *)user_arg, pci_device_value_size ) )
	{
		struct pci_dev* pdev = pci_get_domain_bus_and_slot( 0, PCICFG_GET_BUS(pci_dev_val.BDF), PCICFG_GET_DEVFN(pci_dev_val.BDF));
		if ( pdev )
		{
			int cfg_space_size_bytes = pdev->cfg_size;
			if ( !copy_from_user( buffer4k, (void __user *)(pci_dev_val.Buffer), cfg_space_size_bytes ) )
			{
				int i = 0;
				for ( ; i < cfg_space_size_bytes; ++i, ++buffer4k  )
				{
					retval = pci_write_config_byte( pdev, i, *buffer4k );
					//if ( retval < 0 )
					//	break;
				}
			}
			else
				printk( KERN_INFO "%s: writing cfg space operation: copy_from_user buffer failed\n", PCICFG_MODNAME);
		}
	}
	else
		printk( KERN_INFO "%s: writing cfg space operation: copy_from_user failed\n", PCICFG_MODNAME);

	// Copy result to the user.
	if (retval >= 0)
	{
		if ( copy_to_user( (void __user *)(pci_dev_val.Buffer), (void*)buffer4k, PCICFG_CFGSPACE_LENGTH_MAX_BYTES ) )
		{
			retval = -EINVAL;
			printk( KERN_INFO "%s: writing cfg space operation: copy_to_user failed\n", PCICFG_MODNAME);
		}
	}
	else
		printk( KERN_INFO "%s: writing cfg space failed\n", PCICFG_MODNAME);

	kfree( buffer4k );

	return retval;
}


int GetCfgSpace( unsigned short bdf, unsigned int offset_bytes, unsigned int size_bytes, unsigned char* out_buffer )
{
	int retval = -EINVAL;
	struct pci_dev* pdev = 0;

	if (!out_buffer)
		return -EINVAL;

	if ( offset_bytes >= PCICFG_CFGSPACE_LENGTH_MAX_BYTES ||
	   (offset_bytes + size_bytes) > PCICFG_CFGSPACE_LENGTH_MAX_BYTES )
		return -EINVAL;

	pdev = pci_get_domain_bus_and_slot( 0, PCICFG_GET_BUS(bdf), PCICFG_GET_DEVFN(bdf));
	if ( pdev )
	{
		int i = 0;
		size_bytes = ( size_bytes > pdev->cfg_size ) ? pdev->cfg_size : size_bytes;
		for ( ; i < size_bytes; ++i, ++out_buffer  )
		{
			retval = pci_read_config_byte( pdev, offset_bytes + i, out_buffer );
			//if ( retval < 0 )
			//	break;
		}
	}

	return retval;
}


const struct file_operations pcicfg_fops =
{
   .owner          = THIS_MODULE,
   .open           = PciCfgOpen,
   .release        = PciCfgRelease,
   .read           = PciCfgRead,
   .unlocked_ioctl = PciCfgIoctl
};


/*******************************************************************************
**** Module init/exit implementation
*******************************************************************************/

static int __init PciCfgInit( void )
{
	int ret = register_chrdev( PCICFG_MAJOR, PCICFG_MODNAME, &pcicfg_fops );
	mutex_init(&lock_mutex);
	if( ret < 0 )
		printk( KERN_ERR "Can not register char device \"%s\"\n", PCICFG_MODNAME );
	else
		printk( KERN_INFO "Registered char device\"%s\"", PCICFG_MODNAME );

	return ret;
}


static void __exit PciCfgExit( void )
{
	unregister_chrdev( PCICFG_MAJOR, PCICFG_MODNAME );
	mutex_destroy(&lock_mutex);
	printk( "\"%s\" exited.\n", PCICFG_MODNAME );
}

module_init( PciCfgInit );
module_exit( PciCfgExit );
